diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js index 6d4ca720d..3ba14074a 100644 --- a/web/media/multimedia-modal.react.js +++ b/web/media/multimedia-modal.react.js @@ -1,83 +1,109 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { XCircle as XCircleIcon } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react.js'; +import type { EncryptedMediaType, MediaType } from 'lib/types/media-types.js'; +import EncryptedMultimedia from './encrypted-multimedia.react.js'; import css from './media.css'; +type MediaInfo = + | { + +type: MediaType, + +uri: string, + } + | { + +type: EncryptedMediaType, + +holder: string, + +encryptionKey: string, + }; + type BaseProps = { - +type: string, - +uri: string, + +media: MediaInfo, }; type Props = { ...BaseProps, +popModal: (modal: ?React.Node) => void, }; class MultimediaModal extends React.PureComponent { overlay: ?HTMLDivElement; componentDidMount() { invariant(this.overlay, 'overlay ref unset'); this.overlay.focus(); } render(): React.Node { let mediaModalItem; - if (this.props.type === 'photo') { - mediaModalItem = ; - } else { + const { media } = this.props; + if (media.type === 'photo') { + mediaModalItem = ; + } else if (media.type === 'video') { mediaModalItem = ( ); + } else { + invariant( + media.type === 'encrypted_photo' || media.type === 'encrypted_video', + 'invalid media type', + ); + const { type, holder, encryptionKey } = media; + mediaModalItem = ( + + ); } return (
{mediaModalItem}
); } overlayRef: (overlay: ?HTMLDivElement) => void = overlay => { this.overlay = overlay; }; onBackgroundClick: (event: SyntheticEvent) => void = event => { if (event.target === this.overlay) { this.props.popModal(); } }; onKeyDown: (event: SyntheticKeyboardEvent) => void = event => { if (event.key === 'Escape') { this.props.popModal(); } }; } function ConnectedMultiMediaModal(props: BaseProps): React.Node { const modalContext = useModalContext(); return ; } export default ConnectedMultiMediaModal; diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js index f81df7b18..8b029b7a3 100644 --- a/web/media/multimedia.react.js +++ b/web/media/multimedia.react.js @@ -1,154 +1,155 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { CircularProgressbar } from 'react-circular-progressbar'; import 'react-circular-progressbar/dist/styles.css'; import { XCircle as XCircleIcon, AlertCircle as AlertCircleIcon, } from 'react-feather'; import { useModalContext, type PushModal, } from 'lib/components/modal-provider.react.js'; import type { MediaType } from 'lib/types/media-types.js'; import css from './media.css'; import MultimediaModal from './multimedia-modal.react.js'; import Button from '../components/button.react.js'; import { type PendingMultimediaUpload } from '../input/input-state.js'; type BaseProps = { +uri: string, +type: MediaType, +pendingUpload?: ?PendingMultimediaUpload, +remove?: (uploadID: string) => void, +multimediaCSSClass: string, +multimediaImageCSSClass: string, }; type Props = { ...BaseProps, +pushModal: PushModal, }; class Multimedia extends React.PureComponent { componentDidUpdate(prevProps: Props) { const { uri, pendingUpload } = this.props; if (uri === prevProps.uri) { return; } if ( (!pendingUpload || pendingUpload.uriIsReal) && (!prevProps.pendingUpload || !prevProps.pendingUpload.uriIsReal) ) { URL.revokeObjectURL(prevProps.uri); } } render(): React.Node { let progressIndicator, errorIndicator, removeButton; const { pendingUpload, remove, type, uri, multimediaImageCSSClass, multimediaCSSClass, } = this.props; if (pendingUpload) { const { progressPercent, failed } = pendingUpload; if (progressPercent !== 0 && progressPercent !== 1) { const outOfHundred = Math.floor(progressPercent * 100); const text = `${outOfHundred}%`; progressIndicator = ( ); } if (failed) { errorIndicator = ( ); } if (remove) { removeButton = ( ); } } const imageContainerClasses = [ css.multimediaImage, multimediaImageCSSClass, ]; imageContainerClasses.push(css.clickable); let mediaNode; if (type === 'photo') { mediaNode = ( ); } else { mediaNode = (
); } const containerClasses = [css.multimedia, multimediaCSSClass]; return ( {mediaNode} {progressIndicator} {errorIndicator} ); } remove: (event: SyntheticEvent) => void = event => { event.stopPropagation(); const { remove, pendingUpload } = this.props; invariant( remove && pendingUpload, 'Multimedia cannot be removed as either remove or pendingUpload ' + 'are unspecified', ); remove(pendingUpload.localID); }; onClick: () => void = () => { const { pushModal, type, uri } = this.props; - pushModal(); + const mediaInfo = { type, uri }; + pushModal(); }; } function ConnectedMultimediaContainer(props: BaseProps): React.Node { const modalContext = useModalContext(); return ; } export default ConnectedMultimediaContainer; diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.react.js b/web/modals/threads/gallery/thread-settings-media-gallery.react.js index 9f273b2a9..fcbb0bea6 100644 --- a/web/modals/threads/gallery/thread-settings-media-gallery.react.js +++ b/web/modals/threads/gallery/thread-settings-media-gallery.react.js @@ -1,135 +1,144 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; import { fetchThreadMedia } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { Media } from 'lib/types/media-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; import css from './thread-settings-media-gallery.css'; import Tabs from '../../../components/tabs.react.js'; import MultimediaModal from '../../../media/multimedia-modal.react.js'; import Modal from '../../modal.react.js'; type MediaGalleryTab = 'All' | 'Images' | 'Videos'; type ThreadSettingsMediaGalleryModalProps = { +onClose: () => void, +parentThreadInfo: ThreadInfo, +limit: number, +activeTab: MediaGalleryTab, }; function ThreadSettingsMediaGalleryModal( props: ThreadSettingsMediaGalleryModalProps, ): React.Node { const { pushModal } = useModalContext(); const { onClose, parentThreadInfo, limit, activeTab } = props; const { id: threadID } = parentThreadInfo; const modalName = 'Media'; const callFetchThreadMedia = useServerCall(fetchThreadMedia); const [mediaInfos, setMediaInfos] = React.useState([]); const [tab, setTab] = React.useState(activeTab); React.useEffect(() => { const fetchData = async () => { const result = await callFetchThreadMedia({ threadID, limit, offset: 0, }); setMediaInfos(result.media); }; fetchData(); }, [callFetchThreadMedia, threadID, limit]); const onClick = React.useCallback( (media: Media) => { - invariant( - media.type === 'photo' || media.type === 'video', - ' supports only unencrypted images and videos', - ); - pushModal(); + // This branching is needed for Flow. + let mediaInfo; + if (media.type === 'photo' || media.type === 'video') { + mediaInfo = { + type: media.type, + uri: media.uri, + }; + } else { + mediaInfo = { + type: media.type, + holder: media.holder, + encryptionKey: media.encryptionKey, + }; + } + pushModal(); }, [pushModal], ); const filteredMediaInfos = React.useMemo(() => { if (tab === 'Images') { return mediaInfos.filter(mediaInfo => mediaInfo.type === 'photo'); } else if (tab === 'Videos') { return mediaInfos.filter(mediaInfo => mediaInfo.type === 'video'); } return mediaInfos; }, [tab, mediaInfos]); const mediaCoverPhotos = React.useMemo( () => filteredMediaInfos.map(media => media.thumbnailURI || media.uri), [filteredMediaInfos], ); const mediaGalleryItems = React.useMemo( () => filteredMediaInfos.map((media, i) => (
onClick(media)} className={css.mediaContainer} >
)), [filteredMediaInfos, onClick, mediaCoverPhotos], ); const handleScroll = React.useCallback( async event => { const container = event.target; // Load more data when the user is within 1000 pixels of the end const buffer = 1000; if ( container.scrollHeight - container.scrollTop > container.clientHeight + buffer ) { return; } const result = await callFetchThreadMedia({ threadID, limit, offset: mediaInfos.length, }); setMediaInfos([...mediaInfos, ...result.media]); }, [callFetchThreadMedia, threadID, limit, mediaInfos], ); return (
{mediaGalleryItems}
{mediaGalleryItems}
{mediaGalleryItems}
); } export default ThreadSettingsMediaGalleryModal;